iT邦幫忙

2022 iThome 鐵人賽

DAY 29
0
Mobile Development

在 iOS 開發路上的大小事2系列 第 29

【在 iOS 開發路上的大小事2-Day29】來自 Apple 爸爸的最新力作 - Swift Charts 之 BarMark 實作篇

  • 分享至 

  • xImage
  •  

上一篇介紹了 RuleMark 的實作,今天要來介紹的是 Swift Charts 系列中的最後一個圖表 BarMark

BarMark 一共提供了七種 init 的方法,讓開發者可以繪製不同樣式的圖表

public init<X, Y>(x: PlottableValue<X>, 
                  y: PlottableValue<Y>, 
                  width: MarkDimension = .automatic, 
                  height: MarkDimension = .automatic, 
                  stacking: MarkStackingMethod = .standard) where X : Plottable, Y : Plottable

public init<X>(x: PlottableValue<X>, 
               yStart: CGFloat? = nil, 
               yEnd: CGFloat? = nil, 
               width: MarkDimension = .automatic, 
               stacking: MarkStackingMethod = .standard) where X : Plottable

public init<Y>(xStart: CGFloat? = nil, 
               xEnd: CGFloat? = nil, 
               y: PlottableValue<Y>, 
               height: MarkDimension = .automatic, 
               stacking: MarkStackingMethod = .standard) where Y : Plottable

public init<X, Y>(xStart: PlottableValue<X>, 
                  xEnd: PlottableValue<X>, 
                  y: PlottableValue<Y>, 
                  height: MarkDimension = .automatic) where X : Plottable, Y : Plottable

public init<X>(xStart: PlottableValue<X>, 
               xEnd: PlottableValue<X>, 
               yStart: CGFloat? = nil, 
               yEnd: CGFloat? = nil) where X : Plottable

public init<X, Y>(x: PlottableValue<X>, 
                  yStart: PlottableValue<Y>, 
                  yEnd: PlottableValue<Y>, 
                  width: MarkDimension = .automatic) where X : Plottable, Y : Plottable

public init<Y>(xStart: CGFloat? = nil, 
               xEnd: CGFloat? = nil, 
               yStart: PlottableValue<Y>, 
               yEnd: PlottableValue<Y>) where Y : Plottable

Model

import SwiftUI

struct DepartmentEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var department: String
    
    var profit: Int
}

ViewModel

import SwiftUI

class DepartmentEntityViewModel {
    
    var departmentData: [DepartmentEntity] = [
        .init(department: "Production", profit: 15000),
        .init(department: "Marketing", profit: 8000),
        .init(department: "Finance", profit: 10000)
    ]
}

View

這邊要記得 import Charts,因為我們要顯示 BarMark 在畫面上

然後這邊宣告了一個 ViewModel 的變數 deVM
並在前面加上 @State 修飾字,讓 SwiftUI 來幫我們管理 ViewModel 狀態

接著是 Charts 的語法,語法也是很簡單,像是下面這樣

@State private var deVM = DepartmentEntityViewModel()

// 1:deVM.departmentData,圖表的資料來源
Chart(deVM.departmentData) {
    BarMark(
        x: .value("Department", $0.department), // 2:x 軸要顯示的資料
        y: .value("Profit", $0.profit) // 3:y 軸要顯示的資料
    )
}
.frame(height: 300)
.padding()

或者你也可以透過 ForEach 來寫,只是就會要讓 Model 繼承 Identifiable
並宣告 UUID() 變數在 Model 裡面,像是這樣 var id = UUID().uuidString

@State private var deVM = DepartmentEntityViewModel()

Chart {
    // 1:deVM.departmentData,圖表的資料來源
    ForEach(deVM.departmentData) { department in
        BarMark(
            x: .value("Department", department.department), // 2:x 軸要顯示的資料
            y: .value("Profit", department.profit) // 3:y 軸要顯示的資料
        )
    }
}
.frame(height: 300)
.padding()

現在的圖,應該會長得像下面這樣

BarMark

如果要將每個 Bar 都顯示對應數值的話,可以透過 .annotation 這個 modifier

@State private var deVM = DepartmentEntityViewModel()

Chart {
    ForEach(deVM.departmentData) { department in
        BarMark(
            x: .value("Department", department.department),
            y: .value("Profit", department.profit)
        )
        .annotation {
            Text("\(department.profit)")
        }
    }
}
.chartYAxisLabel("Normal", alignment: .center)
.frame(height: 300)
.padding()

加完後,會長得像下面這樣

BarMark + annotation modifier

堆疊樣式的 BarMark

接下來還有像是堆疊樣式的 BarMark

讓我們先來改寫一下

Model

import SwiftUI

struct DepartmentCategoryEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var department: String
    
    var profit: Double
    
    var category: String
}

ViewModel

import SwiftUI

class DepartmentCategoryEntityViewModel {
    
    var departmentData: [DepartmentCategoryEntity] = [
        .init(department: "Production", profit: 4000, category: "Gizmos"),
        .init(department: "Production", profit: 5000, category: "Gadgets"),
        .init(department: "Production", profit: 6000, category: "Widgets"),
        .init(department: "Marketing", profit: 2000, category: "Gizmos"),
        .init(department: "Marketing", profit: 1000, category: "Gadgets"),
        .init(department: "Marketing", profit: 5000, category: "Widgets"),
        .init(department: "Finance", profit: 2000, category: "Gizmos"),
        .init(department: "Finance", profit: 3000, category: "Gadgets"),
        .init(department: "Finance", profit: 5000, category: "Widgets")
    ]
}

View

@State private var dceVM = DepartmentCategoryEntityViewModel()

Chart {
    ForEach(dceVM.departmentData) { department in
        BarMark(
            x: .value("Category", department.department),
            y: .value("Profit", department.profit),
            stacking: .standard
        )
        .foregroundStyle(by: .value("Product Category", department.category))
    }
}
.chartYAxisLabel("Stacking.standard", alignment: .center)
.frame(height: 300)
.padding()

這邊有一個 optional 參數 stacking: 可以改變 BarMark 的堆疊樣式

stacking 樣式一共有四種,standard (預設值)normalizedcenterunstacked
可以依照自己的需求,來改變 BarMark 的顯示方式

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
@frozen public struct MarkStackingMethod : Equatable {

    /// Stack marks starting at zero.
    ///
    /// Negative values appear below zero, creating diverging stacked marks.
    @inlinable public static var standard: MarkStackingMethod { get }

    /// Create normalized stacked bar and area charts.
    @inlinable public static var normalized: MarkStackingMethod { get }

    /// Stack marks using a center offset.
    ///
    /// Use this type to create a stream graph.
    @inlinable public static var center: MarkStackingMethod { get }

    /// Don't stack marks.
    @inlinable public static var unstacked: MarkStackingMethod { get }
}

一維樣式的 BarMark

在 手機設定 -> 一般 -> iPhone 儲存空間 裡面,會看到最上面有一條柱狀圖

iPhone 儲存空間的柱狀圖

那我們要如何繪製一個類似的圖表呢?這時候就可以透過 BarMark 不同的 init 來做到

public init<X>(x: PlottableValue<X>, 
               yStart: CGFloat? = nil, 
               yEnd: CGFloat? = nil, 
               width: MarkDimension = .automatic, 
               stacking: MarkStackingMethod = .standard) where X : Plottable

Model

import SwiftUI

struct FileCategoryEntity: Identifiable {
    
    var id = UUID().uuidString
    
    var fileSizePercent: Double
    
    var fileCategory: String
}

ViewModel

import SwiftUI

class FileCategoryEntityViewModel {
    
    var fileData: [FileCategoryEntity] = [
        .init(fileSizePercent: 20, fileCategory: "App"),
        .init(fileSizePercent: 40, fileCategory: "照片"),
        .init(fileSizePercent: 5, fileCategory: "媒體"),
        .init(fileSizePercent: 10, fileCategory: "訊息"),
        .init(fileSizePercent: 12, fileCategory: "iOS"),
        .init(fileSizePercent: 13, fileCategory: "系統資料"),
    ]
}

View

@State private var vm = FileCategoryEntityViewModel()

Chart {
    ForEach(vm.fileData) { file in
        BarMark(
            x: .value("File Size Percent", file.fileSizePercent)
        )
        .foregroundStyle(by: .value("File Category", file.fileCategory))
    }
}
.chartXAxis(.hidden)
.frame(height: 100)
.padding()

現在的圖,應該會長得像下面這樣

1D BarMark

完整程式碼 (BarMark)

import SwiftUI
import Charts

struct BarMarkView: View {
    
    @State private var deVM = DepartmentEntityViewModel()
        
    var body: some View {
        Chart {
            ForEach(deVM.departmentData) { department in
                BarMark(
                    x: .value("Department", department.department),
                    y: .value("Profit", department.profit)
                )
                .annotation {
                    Text("\(department.profit)")
                }
            }
        }
        .chartYAxisLabel("Normal", alignment: .center)
        .frame(height: 300)
        .padding()
    }
}

struct BarChartView_Previews: PreviewProvider {
    static var previews: some View {
        BarMarkView()
    }
}

完整程式碼 (堆疊樣式的 BarMark)

import SwiftUI
import Charts

struct BarMarkView: View {
        
    @State private var dceVM = DepartmentCategoryEntityViewModel()
    
    var body: some View {
        Chart {
            ForEach(dceVM.departmentData) { department in
                BarMark(
                    x: .value("Category", department.department),
                    y: .value("Profit", department.profit),
                    stacking: .standard
                )
                .foregroundStyle(by: .value("Product Category", department.category))
            }
        }
        .chartYAxisLabel("Stacking.standard", alignment: .center)
        .frame(height: 300)
        .padding()
    }
}

struct BarChartView_Previews: PreviewProvider {
    static var previews: some View {
        BarMarkView()
    }
}

完整程式碼 (一維樣式的 BarMark)

import SwiftUI
import Charts

struct OneDBarMarkView: View {

    @State private var vm = FileCategoryEntityViewModel()
    
    var body: some View {
        Chart {
            ForEach(vm.fileData) { file in
                BarMark(
                    x: .value("File Size Percent", file.fileSizePercent)
                )
                .foregroundStyle(by: .value("File Category", file.fileCategory))
            }
        }
        .chartXAxis(.hidden)
        .frame(height: 100)
        .padding()
    }
}

struct OneDBarMarkView_Previews: PreviewProvider {
    static var previews: some View {
        OneDBarMarkView()
    }
}

總結

這篇簡單實作了 Swift Charts 中的 BarMark

在這幾篇的 Swift Charts 實作中,我個人覺得 Swift Charts 算是滿容易上手的,功能也算多

唯一美中不足的部分可能就是只支援 SwiftUI,但現在 UIKit 也可以透過 UIHostingController 來串接 SwiftUI 的畫面,所以說還可以啦?

期待之後 Apple 為 Swift Charts 加入更多可玩性!

在這幾篇所實作的 Swift Charts 的完整程式碼,可以到我的 GitHub 上找到喔~

參考資料

  1. https://developer.apple.com/documentation/charts/barmark

上一篇
【在 iOS 開發路上的大小事2-Day28】來自 Apple 爸爸的最新力作 - Swift Charts 之 RuleMark 實作篇
下一篇
【在 iOS 開發路上的大小事2-Day30】總結!
系列文
在 iOS 開發路上的大小事230
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言